/*
 * @Date: 2023-02-24 11:06:26
 * @LastEditTime: 2023-03-22 11:04:53
 * @FilePath: /hash-guess-game/src/sdk/solana.ts
 * 介绍:
 */
import { sdkSucceed, sdkFail, SdkRes } from "./index";
import { sdkWalletPlatformInfo, WalletPlatformInfoRetrun } from "./tp";
import type { RpcResponseAndContext, TokenAmount } from "@solana/web3.js";
import type * as SolanaWeb3 from "@solana/web3.js";
import solanaWeb3 from "./solanaWeb3.js";
import type * as SplToken from "@solana/spl-token";
import splToken from "./spl-token.js";

const SystemProgram: typeof SolanaWeb3.SystemProgram = solanaWeb3.SystemProgram;
const Transaction: typeof SolanaWeb3.Transaction = solanaWeb3.Transaction;
const PublicKey: typeof SolanaWeb3.PublicKey = solanaWeb3.PublicKey;
const Connection: typeof SolanaWeb3.Connection = solanaWeb3.Connection;

//*兼容bitKeep钱包
if (window.bitkeep?.solana) {
  window.solana = window.bitkeep.solana;
}
let provider = import.meta.env.VITE_SOLANA_NODE;
const connection = new Connection(provider);

/**
 * * 连接钱包
 */
export function sdkConnect(): Promise<SdkRes<any>> {
  return new Promise((resolve) => {
    if (!window.solana) return resolve(sdkFail("请切换solana链后登录"));
    if (window.solana.publicKey)
      return resolve(sdkSucceed("已经连接", window.solana.publicKey));
    window.solana
      .connect()
      .then((res: any) => {
        resolve(sdkSucceed("连接钱包", res));
      })
      .catch((error: any) => {
        resolve(sdkFail("意料之外:" + error?.message, error));
      });
  });
}
/**
 * * 获取钱包信息
 */
export function sdkWalletInfo() {
  return new Promise<SdkRes<WalletInfoReturn | void>>(async (resolve) => {
    if (!window.solana) return resolve(sdkFail("请切换solana链后登录"));
    try {
      const fromPubKey = window.solana.publicKey;
      /**私钥 */
      const accounts: string = fromPubKey.toBase58();
      const walletPlatformInfo = await sdkWalletPlatformInfo();
      return resolve(
        sdkSucceed<WalletInfoReturn>("获取钱包信息", {
          accounts,
          ...walletPlatformInfo.data,
        })
      );
    } catch (error: any) {
      return resolve(sdkFail("意料之外:" + error?.message, error));
    }
  });
}
interface WalletInfoReturn extends WalletPlatformInfoRetrun {
  accounts: string;
}
/**
 * 获取代币余额
 * @param mint 合约地址
 * @param findAccount 查询用户地址
 */
export function sdkFindTokenBalance(mint: string, findAccount?: string) {
  console.log("获取币种余额 --token", mint);
  return new Promise<SdkRes<SucceedRes | void>>(async (resolve) => {
    try {
      findAccount ??= sdkGetAccount()?.toBase58();
      if (!findAccount) return resolve(sdkFail("获取当前用户公钥失败"));
      const mintPublicKey = new PublicKey(mint);
      const res = await sdkFindTokenAccount(
        mintPublicKey,
        new PublicKey(findAccount)
      );

      if (!res.data) return resolve(sdkFail("创建代币账户"));
      connection
        .getTokenAccountBalance(new PublicKey(res.data))
        .then((res) => {
          resolve(
            sdkSucceed("查询代币余额结果", {
              ...res,
              balance: formatBalance(res.value.amount, res.value.decimals),
            })
          );
        })
        .catch((error) => {
          return resolve(sdkFail("查询代币余额时犯错", error));
        });
    } catch (error) {
      return resolve(sdkFail("获取币种余额失败" + " --token", { mint, error }));
    }
  });
}
interface SucceedRes extends RpcResponseAndContext<TokenAmount> {
  /**余额 */
  balance: number;
}
/**
 * * 获取主币余额
 * @param findAccount 查询用户地址
 */
export function sdkFindBalance(findAccount?: string) {
  return new Promise<SdkRes<number | void>>((resolve) => {
    console.log("获取币种余额 --main");
    findAccount ??= sdkGetAccount()?.toBase58();
    if (!findAccount) return resolve(sdkFail("获取当前用户公钥失败", findAccount));
    connection
      .getBalance(new PublicKey(findAccount))
      .then((res) => {
        resolve(sdkSucceed("查询主币余额成功", formatBalance(res, 9)));
      })
      .catch((error) => {
        resolve(sdkFail("获取余额失败" + " --main", error));
      });
  });
}
/**
 * * 查询币种余额,自适应主币与代币
 * @param mint 合约地址
 * @param findAccount 查询账户
 */
export async function sdkAutoFindBalance(
  mint: void | null | string,
  findAccount?: string
): Promise<SdkRes<number | void>> {
  if (mint) {
    const res = await sdkFindTokenBalance(mint, findAccount);
    //*对成功数据进行处理
    if (res.data) return sdkSucceed(res.message, res.data.balance);
    return sdkFail(res.message);
  }
  return sdkFindBalance(findAccount);
}
/**获取当前账户公钥 */
export function sdkGetAccount(): InstanceType<typeof PublicKey> | undefined {
  return window.solana?.publicKey;
}

/**
 * * 查询代币账户
 * @param mint 合约公钥
 * @param findPublicKey 查询公钥
 */
export function sdkFindTokenAccount(
  mintPublicKey: InstanceType<typeof PublicKey>,
  findPublicKey?: InstanceType<typeof PublicKey>
): Promise<SdkRes<SolanaWeb3.PublicKey | void>> {
  return new Promise((resolve) => {
    findPublicKey ??= sdkGetAccount();
    if (!findPublicKey) return resolve(sdkFail("获取当前用户公钥失败"));
    const token = new splToken.Token(
      connection,
      mintPublicKey,
      splToken.TOKEN_PROGRAM_ID,
      findPublicKey
    );
    token
      .getOrCreateAssociatedAccountInfo(findPublicKey)
      .then((res: AssociatedAccountInfo) => {
        resolve(sdkSucceed("查询代币账户成功", res.address));
      })
      .catch((err: Error) => {
        resolve(sdkFail(err.message, err));
      });
  });
}
/**
 * * 验证代币账户是否存在
 * @param findPublicKey 查询公钥
 */
export function sdkVerifyAccount(
  findPublicKey: InstanceType<typeof PublicKey>
) {
  return new Promise<boolean>((resolve) => {
    connection
      .getAccountInfo(findPublicKey)
      .then((res) => {
        if (!res) {
          console.log("验证用户不存在", res);
          return resolve(false);
        }
        console.log("验证用户存在", res);
        resolve(true);
      })
      .catch((error) => {
        console.log("验证用户不存在", error);
        resolve(false);
      });
  });
}
/**
 * * 发起代币转账
 * @param mint 合约地址
 * @param amount 付款金额
 * @param format 精度
 * @param toAccount 收款地址
 * @param formAccount 付款地址
 */
export function sdkTransferToken(
  mint: string,
  amount: number,
  format: number,
  toAccount: string,
  formAccount?: string
) {
  return new Promise<SdkRes<void | string>>(async (resolve) => {
    console.log("发起代币转账", mint, amount);
    try {
      formAccount ??= sdkGetAccount()?.toBase58();
      if (!formAccount) return resolve(sdkFail("获取当前用户公钥失败"));
      const money = formatBalance(amount, format, "*");
      if (money === "--") return resolve(sdkFail("金额错误", money));
      /**付款方公钥 */
      const fromPubKey = new PublicKey(formAccount);
      /**收款方公钥 */
      const toPubKey = new PublicKey(toAccount);
      /**合约公钥 */
      const mintPubkey = new PublicKey(mint);
      /**Token实例 */

      const [formRes, toRes] = await Promise.all([
        sdkFindTokenAccount(mintPubkey, fromPubKey),
        sdkFindTokenAccount(mintPubkey, toPubKey),
      ]);
      resolve(
        structureTransaction(
          mintPubkey,
          money,
          {
            pubKey: fromPubKey,
            tokenPubKey: formRes.data || undefined,
          },
          {
            pubKey: toPubKey,
            tokenPubKey: toRes.data || undefined,
          }
        )
      );
    } catch (error: any) {
      resolve(sdkFail("意料之外" + error.message || "", error));
    }
  });
}
/**
 * * 构建代币转账
 * @param formatAmount 已经过精度处理的amount
 * @param from 付款者信息
 * @param to 收款者信息
 */
function structureTransaction(
  mintPubkey: SolanaWeb3.PublicKey,
  formatAmount: number,
  from: {
    tokenPubKey?: SolanaWeb3.PublicKey;
    pubKey: SolanaWeb3.PublicKey;
  },
  to: { tokenPubKey?: SolanaWeb3.PublicKey; pubKey: SolanaWeb3.PublicKey }
) {
  return new Promise<SdkRes<string | void>>(async (resolve) => {
    const transaction = new Transaction();
    if (!from.tokenPubKey) return resolve(sdkFail("获取付款方代币地址失败"));
    if (!to.tokenPubKey) {
      return resolve(sdkFail("获取收款方代币地址失败"));
      transaction.add(
        splToken.Token.createAssociatedTokenAccountInstruction(
          splToken.ASSOCIATED_TOKEN_PROGRAM_ID,
          splToken.TOKEN_PROGRAM_ID,
          mintPubkey,
          to.pubKey,
          to.pubKey,
          from.pubKey
        )
      );
    }
    transaction.add(
      splToken.Token.createTransferInstruction(
        splToken.TOKEN_PROGRAM_ID,
        from.tokenPubKey,
        to.tokenPubKey,
        from.pubKey,
        [],
        formatAmount
      )
    );
    transaction.feePayer = from.pubKey;
    console.info("toTokenAddress", to.tokenPubKey?.toBase58());
    console.info("fromTokenAddress", from.tokenPubKey?.toBase58());
    console.info("fromAddress", from.pubKey.toBase58());
    console.info("formatAmount", formatAmount);
    connection.getRecentBlockhash("max").then((res) => {
      // Latest transaction hash(最新的交易hash)
      transaction.recentBlockhash = res.blockhash;
      window.solana
        ?.signTransaction(transaction)
        .then(sendTransaction)
        .catch((error: Error) => {
          resolve(sdkFail("签名失败" + error.message, error));
        });
    });
    function sendTransaction(signed: any) {
      connection
        .sendRawTransaction(signed.serialize())
        .then(returnTxid)
        .catch((error: Error) => {
          resolve(sdkFail("发送交易失败" + error.message, error));
        });
    }
    /**返回交易哈希 */
    function returnTxid(txid: any) {
      resolve(sdkSucceed("转账token成功", txid));
    }
  });
}

/**
 * * 发起主币转账
 * @param mint 合约地址
 * @param amount 付款金额
 * @param format 精度
 * @param toAccount 收款地址
 * @param formAccount 付款地址
 */
export function sdkTransferMain(
  amount: number,
  format: number,
  toAccount: string,
  formAccount?: string
) {
  return new Promise<SdkRes<void | string>>(async (resolve) => {
    console.log("发起主币转账", amount);
    try {
      formAccount ??= sdkGetAccount()?.toBase58();
      if (!formAccount) return resolve(sdkFail("获取当前用户公钥失败"));
      /**付款方公钥 */
      const fromPubKey = new PublicKey(formAccount);
      /**收款方公钥 */
      const toPubKey = new PublicKey(toAccount);
      const transaction = new Transaction();

      transaction.add(
        SystemProgram.transfer({
          fromPubkey: fromPubKey,
          toPubkey: toPubKey,
          lamports: Number(formatBalance(amount, format, "*")),
        })
      );
      /**添加付款人 */
      transaction.feePayer = fromPubKey;
      connection
        .getRecentBlockhash()
        .then((res) => {
          // Latest transaction hash(最新的交易hash)
          transaction.recentBlockhash = res.blockhash;
          window.solana
            ?.signTransaction(transaction)
            .then((signed: any) => {
              connection
                .sendRawTransaction(signed.serialize())
                .then((txid: any) => {
                  resolve(sdkSucceed("转账main成功", txid));
                })
                .catch((error: Error) => {
                  resolve(sdkFail("发送交易时失败" + error.message, error));
                });
            })
            .catch((error: Error) => {
              resolve(sdkFail("签名时失败" + error.message, error));
            });
        })
        .catch((error) => {
          resolve(sdkFail("最新交易hash获取失败" + error.message, error));
        });
    } catch (error: any) {
      resolve(sdkFail("意料之外" + error.message || "", error));
    }
  });
}
/**
 * * 自适应发起转账
 * @param mint 合约地址
 * @param amount 付款金额
 * @param format 精度
 * @param toAccount 收款地址
 * @param formAccount 付款地址
 */
export async function sdkAutoTransfer(
  mint: string | null | void,
  amount: number,
  format: number,
  toAccount: string,
  formAccount?: string
) {
  if (mint)
    return await sdkTransferToken(mint, amount, format, toAccount, formAccount);
  return await sdkTransferMain(amount, format, toAccount, formAccount);
}
/**金额精度调整 */
function formatBalance(
  balance: number | string | void | null,
  format: number,
  mode: "*" | "/" = "/"
): number | "--" {
  if (!balance && Number(balance) !== 0) return "--";
  balance = Number(balance);
  if (isNaN(balance)) return "--";
  if (mode == "/") return balance / Math.pow(10, format);
  return balance * Math.pow(10, format);
}

interface AssociatedAccountInfo {
  address: InstanceType<typeof PublicKey>;
  amount: { negative: number; words: number[]; length: number; red: any };
  closeAuthority: any;
  closeAuthorityOption: number;
  delegate: any;
  delegateOption: number;
  delegatedAmount: {
    negative: number;
    words: number[];
    length: number;
    red: any;
  };
  isFrozen: boolean;
  isInitialized: boolean;
  isNative: boolean;
  isNativeOption: number;
  mint: InstanceType<typeof PublicKey>;
  owner: InstanceType<typeof PublicKey>;
  rentExemptReserve: any;
  state: number;
}
